
from torch.autograd import Variable
from torchvision.utils import save_image
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import os, random
import time
import datetime

from CLIP import clip
from CLIP.model import convert_weights
from model import Generator
from model import Discriminator
# from model import ClipEncoder


class Solver(object):
    """Solver for training and testing StarGAN."""

    def __init__(self, data_loader, device, config):
        """Initialize configurations."""

        # Data loader.
        self.data_loader = data_loader
        self.device = device

        # Model configurations.
        self.clip_model_name = config.clip_model_name
        self.image_size = config.image_size
        self.emb_dim = config.emb_dim
        self.g_conv_dim = config.g_conv_dim
        self.d_conv_dim = config.d_conv_dim
        self.g_repeat_num = config.g_repeat_num
        self.d_repeat_num = config.d_repeat_num
        self.lambda_cls = config.lambda_cls
        self.lambda_rec = config.lambda_rec
        self.lambda_gp = config.lambda_gp
        self.use_patch_clip_loss = config.use_patch_clip_loss

        # Training configurations.
        self.batch_size = config.batch_size
        self.num_iters = config.num_iters
        self.num_iters_decay = config.num_iters_decay
        self.g_lr = config.g_lr
        self.d_lr = config.d_lr
        self.clip_lr = config.clip_lr
        self.n_critic = config.n_critic
        self.beta1 = config.beta1
        self.beta2 = config.beta2
        self.resume_iters = config.resume_iters

        # Test configurations.
        self.test_iters = config.test_iters

        # Miscellaneous.
        self.use_tensorboard = config.use_tensorboard

        # Directories.
        self.log_dir = config.log_dir
        self.sample_dir = config.sample_dir
        self.model_save_dir = config.model_save_dir
        self.result_dir = config.result_dir

        # Step size.
        self.log_step = config.log_step
        self.sample_step = config.sample_step
        self.model_save_step = config.model_save_step
        self.lr_update_step = config.lr_update_step

        # Build the model and tensorboard.
        self.build_model()
        self.bce = nn.BCELoss()
        if self.use_tensorboard:
            self.build_tensorboard()

    def build_model(self):
        """Create a generator and a discriminator."""
        # self.encoder = ClipEncoder(self.clip_model_name, self.device)
        self.encoder, _ = clip.load(self.clip_model_name, self.device, jit=False)
        self.encoder = self.encoder.float()
        self.encoder.train()
        # for p in self.encoder.parameters():
            # p.data = p.data.float()
            # p.grad.data = p.grad.data.float()
        # convert_weights(self.encoder)
        self.G = Generator(self.emb_dim, self.g_conv_dim, self.g_repeat_num)  
        self.D = Discriminator(self.image_size, self.d_conv_dim, self.emb_dim, self.d_repeat_num)

        self.clip_optimizer = torch.optim.Adam(self.encoder.parameters(), self.clip_lr, [0.9, 0.98], eps=1e-6, weight_decay=0.2)
        self.g_optimizer = torch.optim.Adam(self.G.parameters(), self.g_lr, [self.beta1, self.beta2])
        self.d_optimizer = torch.optim.Adam(self.D.parameters(), self.d_lr, [self.beta1, self.beta2])
        self.print_network(self.G, 'G')
        self.print_network(self.D, 'D')
        
        self.encoder.to(self.device)
        self.G.to(self.device)
        self.D.to(self.device)

    def print_network(self, model, name):
        """Print out the network information."""
        num_params = 0
        for p in model.parameters():
            num_params += p.numel()
        print(model)
        print(name)
        print("The number of parameters: {}".format(num_params))

    def restore_model(self, resume_iters):
        """Restore the trained generator and discriminator."""
        print('Loading the trained models from step {}...'.format(resume_iters))
        clip_path = os.path.join(self.model_save_dir, '{}-clip.ckpt'.format(resume_iters))
        G_path = os.path.join(self.model_save_dir, '{}-G.ckpt'.format(resume_iters))
        D_path = os.path.join(self.model_save_dir, '{}-D.ckpt'.format(resume_iters))
        self.encoder.load_state_dict(torch.load(clip_path, map_location=lambda storage, loc: storage))
        self.G.load_state_dict(torch.load(G_path, map_location=lambda storage, loc: storage))
        self.D.load_state_dict(torch.load(D_path, map_location=lambda storage, loc: storage))

    def build_tensorboard(self):
        """Build a tensorboard logger."""
        from logger import Logger
        self.logger = Logger(self.log_dir)

    def update_lr(self, c_lr, g_lr, d_lr):
        """Decay learning rates of the generator and discriminator."""
        for param_group in self.clip_optimizer.param_groups:
            param_group['lr'] = c_lr
        for param_group in self.g_optimizer.param_groups:
            param_group['lr'] = g_lr
        for param_group in self.d_optimizer.param_groups:
            param_group['lr'] = d_lr

    def reset_grad(self):
        """Reset the gradient buffers."""
        self.clip_optimizer.zero_grad()
        self.g_optimizer.zero_grad()
        self.d_optimizer.zero_grad()

    def denorm(self, x):
        """Convert the range from [-1, 1] to [0, 1]."""
        # out = (x + 1) / 2
        std = torch.FloatTensor((0.26862954, 0.26130258, 0.27577711), device=x.device).view(x.size(0), 3, 1, 1)
        mean = torch.FloatTensor((0.48145466, 0.4578275, 0.40821073), device=x.device).view(x.size(0), 3, 1, 1)
        out = std * x + mean
        return out.clamp_(0, 1)

    def gradient_penalty(self, y, x):
        """Compute gradient penalty: (L2_norm(dy/dx) - 1)**2."""
        weight = torch.ones(y.size()).to(self.device)
        dydx = torch.autograd.grad(outputs=y,
                                   inputs=x,
                                   grad_outputs=weight,
                                   retain_graph=True,
                                   create_graph=True,
                                   only_inputs=True)[0]

        dydx = dydx.view(dydx.size(0), -1)
        dydx_l2norm = torch.sqrt(torch.sum(dydx**2, dim=1))
        return torch.mean((dydx_l2norm-1)**2)

    def real_fake_loss(self, out_src, label=0):
        out = out_src.view(out_src.size(0), out_src.size(1))
        target = torch.ones((out_src.size(0), out_src.size(1))) * label 
        target = target.to(self.device)
        return self.bce(out, target)

    def ce_loss(self, logits_per_image, logits_per_txt):
        labels = torch.arange(logits_per_image.shape[0], device=self.device, dtype=torch.long)
        loss = (
            F.cross_entropy(logits_per_image, labels) + 
            F.cross_entropy(logits_per_txt, labels)
        ) / 2
        return loss

    def clip_loss(self, out_code, label_code):
        out_code_ = out_code / (out_code.clone().norm(dim=-1, keepdim=True))
        label_code_ = label_code / (label_code.clone().norm(dim=-1, keepdim=True))
        loss = (1- torch.cosine_similarity(out_code_, label_code_, dim=1))
        return loss

    def patch_clip_loss(self, out_code, img, patch=3):
        out_code_ = out_code / (out_code.clone().norm(dim=-1, keepdim=True))
        height, width = img.shape[2], img.shape[3]
        patch_height, patch_width = int(height/patch), int(width/patch)
        patch_losses = 0
        for i in range(patch):
            for j in range(patch):
                cur_img = img[:, :, patch_height * i: patch_height*(i+1), patch_width*j: patch_width*(j+1)]
                cur_img = F.interpolate(cur_img, (height, width))
                label_code = self.encoder.encode_image(cur_img)
                label_code_ = label_code / (label_code.clone().norm(dim=-1, keepdim=True))
                loss = (1- torch.cosine_similarity(out_code_, label_code_, dim=1))
                patch_losses += loss
        return patch_losses / (patch * patch)

    def train(self):
        """Train StarGAN within a single dataset."""
        # Fetch fixed inputs for debugging.
        data_iter = iter(self.data_loader)
        x_content_fixed, _, _ = next(data_iter)
        x_content_fixed = x_content_fixed.to(self.device)
        style_fixed_txts = []
        style_fixed_imgs = []
        for _ in range(4):
            _c_x_img, _s_x_img, _s_x_txt = next(data_iter)
            print("fixed_target_txt_label: ", _s_x_txt)
            _s_x_img = _s_x_img.to(self.device)
            style_fixed_imgs.append(_s_x_img)
            style_fixed_txts.append(_s_x_txt)
        x_style_list = [torch.zeros_like(x_content_fixed, device=x_content_fixed.device)] + style_fixed_imgs
        x_style_imgs = torch.cat(x_style_list, dim=3)
        # Learning rate cache for decaying.
        g_lr = self.g_lr
        d_lr = self.d_lr
        c_lr = self.clip_lr

        # Start training from scratch or resume training.
        start_iters = 0
        if self.resume_iters:
            start_iters = self.resume_iters
            self.restore_model(self.resume_iters)

        # Start training.
        print('Start training...')
        start_time = time.time()
        for i in range(start_iters, self.num_iters):

            # =================================================================================== #
            #                             1. Preprocess input data                                #
            # =================================================================================== #

            # Fetch real images and labels.
            try:
                real_img, target_img, target_txt = next(data_iter)
            except:
                data_iter = iter(self.data_loader)
                real_img, target_img, target_txt = next(data_iter)

            # =================================================================================== #
            #                             fine-tune clip                                #
            # =================================================================================== #
            target_img = target_img.to(self.device)
            target_txt = clip.tokenize(target_txt).to(self.device)
            # logits_per_image, logits_per_text = self.encoder(target_img, target_txt)
            # encoder_loss = self.ce_loss(logits_per_image, logits_per_text)
            target_img_code = self.encoder.encode_image(target_img)
            target_txt_code = self.encoder.encode_text(target_txt)
            # encoder_loss = self.clip_loss(target_img_code, target_txt_code)
            # self.reset_grad()
            # encoder_loss.backward(retain_graph=True)
            # self.clip_optimizer.step()

            loss = {}
            # loss["CLIP/ce_loss"] = encoder_loss.item()

            real_img = real_img.to(self.device)
            with torch.no_grad():
                real_img_code = self.encoder.encode_image(real_img)
                # target_img_code = self.encoder.encode_image(target_img)
                # target_txt_code = self.encoder.encode_text(target_txt)
            if random.random() > 0.5:
                target_code = target_img_code
            else:
                target_code = target_txt_code

            # =================================================================================== #
            #                             2. Train the discriminator                              #
            # =================================================================================== #

            # Compute loss with real images.
            out_src, out_code = self.D(real_img)
            # d_loss_real = - torch.mean(out_src)
            d_loss_real = - torch.mean(torch.log(out_src + 1e-6))
            # d_loss_real = self.real_fake_loss(out_src, 1)
            if not self.use_patch_clip_loss:
                d_loss_cls = self.clip_loss(out_code, real_img_code)
            else:
                d_loss_cls = self.patch_clip_loss(out_code, real_img)

            # Compute loss with fake images.
            # print(real_img.type, target_code.type)
            x_fake = self.G(real_img, target_code)
            out_src, _ = self.D(x_fake.detach())
            # d_loss_fake = torch.mean(out_src)
            d_loss_fake = - torch.mean(torch.log(1 - out_src + 1e-6))
            # d_loss_fake = self.real_fake_loss(out_src, 0)

            # Compute loss for gradient penalty.
            # alpha = torch.rand(real_img.size(0), 1, 1, 1).to(self.device)
            # x_hat = (alpha * real_img.data + (1 - alpha) * x_fake.data).requires_grad_(True)
            # out_src, _ = self.D(x_hat)
            # d_loss_gp = self.gradient_penalty(out_src, x_hat)

            # Backward and optimize.
            d_loss = d_loss_real + d_loss_fake + self.lambda_cls * d_loss_cls #+ self.lambda_gp * d_loss_gp
            self.reset_grad()
            d_loss.backward()
            self.d_optimizer.step()

            # Logging.
            loss['D/loss_real'] = d_loss_real.item()
            loss['D/loss_fake'] = d_loss_fake.item()
            loss['D/loss_cls'] = d_loss_cls.item()
            # loss['D/loss_gp'] = d_loss_gp.item()
            
            # =================================================================================== #
            #                               3. Train the generator                                #
            # =================================================================================== #
            
            if (i+1) % self.n_critic == 0:
                # Original-to-target domain.
                x_fake = self.G(real_img, target_code)
                out_src, out_code = self.D(x_fake)
                # g_loss_fake = - torch.mean(out_src)
                g_loss_fake = torch.mean(torch.log(1 - out_src + 1e-6))
                # g_loss_fake = self.real_fake_loss(out_src, 1)
                if not self.use_patch_clip_loss:
                    g_loss_cls = self.clip_loss(out_code, target_code)
                else:
                    g_loss_cls = self.patch_clip_loss(out_code, x_fake)

                # Target-to-original domain.
                x_reconst = self.G(x_fake, real_img_code)
                g_loss_rec = torch.mean(torch.abs(real_img - x_reconst))

                # Backward and optimize.
                g_loss = g_loss_fake + self.lambda_rec * g_loss_rec + self.lambda_cls * g_loss_cls
                self.reset_grad()
                g_loss.backward()
                self.g_optimizer.step()

                # Logging.
                loss['G/loss_fake'] = g_loss_fake.item()
                loss['G/loss_rec'] = g_loss_rec.item()
                loss['G/loss_cls'] = g_loss_cls.item()

            # =================================================================================== #
            #                                 4. Miscellaneous                                    #
            # =================================================================================== #

            # Print out training information.
            if (i+1) % self.log_step == 0:
                et = time.time() - start_time
                et = str(datetime.timedelta(seconds=et))[:-7]
                log = "Elapsed [{}], Iteration [{}/{}]".format(et, i+1, self.num_iters)
                for tag, value in loss.items():
                    log += ", {}: {:.4f}".format(tag, value)
                print(log)

                if self.use_tensorboard:
                    for tag, value in loss.items():
                        self.logger.scalar_summary(tag, value, i+1)

            # Translate fixed images for debugging.
            if (i+1) % self.sample_step == 0:
                with torch.no_grad():
                    # img
                    x_fake_imgs = [x_content_fixed]
                    for img_fixed in style_fixed_imgs:
                        code_fixed = self.encoder.encode_image(img_fixed)
                        x_fake_imgs.append(self.G(x_content_fixed, code_fixed))
                    x_imgs = torch.cat(x_fake_imgs, dim=3)
                    # text
                    x_fake_txts = [x_content_fixed]
                    for txt_fixed in style_fixed_txts:
                        txt_fixed = clip.tokenize(txt_fixed).to(self.device)
                        code_fixed = self.encoder.encode_text(txt_fixed)
                        x_fake_txts.append(self.G(x_content_fixed, code_fixed))
                    x_txts = torch.cat(x_fake_txts, dim=3)

                    x_concat = torch.cat([x_style_imgs, x_imgs, x_txts], dim=2)
                    sample_path = os.path.join(self.sample_dir, '{}-images.jpg'.format(i+1))
                    save_image(self.denorm(x_concat.data.cpu()), sample_path, nrow=self.batch_size, padding=0)
                    print('Saved real and fake images into {}...'.format(sample_path))

            # Save model checkpoints.
            if (i+1) % self.model_save_step == 0:
                clip_path = os.path.join(self.model_save_dir, '{}-clip.ckpt'.format(i+1))
                G_path = os.path.join(self.model_save_dir, '{}-G.ckpt'.format(i+1))
                D_path = os.path.join(self.model_save_dir, '{}-D.ckpt'.format(i+1))
                torch.save(self.encoder.state_dict(), clip_path)
                torch.save(self.G.state_dict(), G_path)
                torch.save(self.D.state_dict(), D_path)
                print('Saved model checkpoints into {}...'.format(self.model_save_dir))

            # Decay learning rates.
            if (i+1) % self.lr_update_step == 0 and (i+1) > (self.num_iters - self.num_iters_decay):
                c_lr -= (self.clip_lr / float(self.num_iters_decay))
                g_lr -= (self.g_lr / float(self.num_iters_decay))
                d_lr -= (self.d_lr / float(self.num_iters_decay))
                self.update_lr(c_lr, g_lr, d_lr)
                print ('Decayed learning rates, c_lr: {}, g_lr: {}, d_lr: {}.'.format(c_lr, g_lr, d_lr))

    def test(self):
        """Translate images using StarGAN trained on a single dataset."""
        # Load the trained generator.
        self.restore_model(self.test_iters)
        
        with torch.no_grad():
            for i, (real_img, target_img, target_txt) in enumerate(self.data_loader):
                target_codes = [
                    self.encoder.encode_image(target_img),
                    self.encoder.encode_text(target_txt)
                ]
                # Prepare input images and target domain labels.
                real_img = real_img.to(self.device)

                # Translate images.
                x_fake_list = [real_img, target_img]
                for c_trg in target_codes:
                    x_fake_list.append(self.G(real_img, c_trg))

                # Save the translated images.
                x_concat = torch.cat(x_fake_list, dim=3)
                result_path = os.path.join(self.result_dir, '{}-images.jpg'.format(i+1))
                save_image(self.denorm(x_concat.data.cpu()), result_path, nrow=self.batch_size, padding=0)
                print('Saved real and fake images into {}...'.format(result_path))

     